/********************************************************************************
  * @file    compileprocess.cpp
  * @author  Lun Li
  * @version V2.2.0
  * @date    2022.7.8
  * @brief   This file provides all CompileProcess functions.
  *******************************************************************************/

#include "compileprocess.h"

#include <QFileInfo>
#include <QRegularExpression>
#include <QTime>
#include <QThread>

CompileProcess::CompileProcess(const ProgramConfig *config, QObject *parent)
    : QProcess(parent),
      m_config(config)
{
    // 命令行本身出现错误
    connect(this, &QProcess::errorOccurred, this, [=]() {
        currentError = errorString();
        emit compileError(currentError);
    });

    // 命令运行时产生错误
    connect(this, &QProcess::readyReadStandardError, this, [=](){
        currentError = QString::fromLocal8Bit(readAllStandardError());
        emit compileError(currentError);
    });

    // 命令运行时产生输出
    connect(this, &QProcess::readyReadStandardOutput, this, [=](){
        currentOutput = QString::fromLocal8Bit(readAllStandardOutput());
        emit compileOutput(currentOutput);
    });

    // 编译开始与结束时，设置开始标记
    connect(this, &CompileProcess::compileStarted, this, [&] {
        m_isStarted = true;
    });
    connect(this, &CompileProcess::compileFinished, this, [&] {
        m_isStarted = false;
        // 从流写入日志文件
        inputStream.flush();
        outputStream.flush();
    });

    // vvp结束时，从流写入日志文件
    connect(this, &CompileProcess::VVPFinished, this, [&] {
        inputStream.flush();
        outputStream.flush();
    });

    //将编译的输入、输出、错误记入到流之中
    connect(this, &CompileProcess::compileInput, this, [&](const QString &input) {
        if (m_isLogEnabled) {
            inputStream << input << "\r\n";
            outputStream << input << "\r\n";
        }
    });
    connect(this, &CompileProcess::compileOutput, this, [&](const QString &output) {
        if (m_isLogEnabled) {
            outputStream << output << "\r\n";
        }
    });
    connect(this, &CompileProcess::compileError, this, [&](const QString &error) {
        if (m_isLogEnabled) {
            outputStream << error << "\r\n";
        }
    });

    // 配置改变时，参数随之改变
    connect(m_config, &ProgramConfig::toolConfigChanged, this, [&] {
        m_isLogEnabled = m_config->isLogEnabled();
        m_isLoadHierarchyEnabled = m_config->isLoadHierarchyEnabled();
    });

    // 全局信号相关
    extern SignalConnection globalSignal;
    connect(&globalSignal, &SignalConnection::compileFilesRequested, this, &CompileProcess::runCompile);
    connect(&globalSignal, &SignalConnection::runFileRequested, this, &CompileProcess::runVVP);
    connect(&globalSignal, &SignalConnection::compileCommandRequested, this, &CompileProcess::runAnyCommand);
    connect(this, &CompileProcess::compileStarted, &globalSignal, &SignalConnection::compileStarted);
    connect(this, &CompileProcess::compileInput, &globalSignal, &SignalConnection::compileInput);
    connect(this, &CompileProcess::compileOutput, &globalSignal, &SignalConnection::compileOutput);
    connect(this, &CompileProcess::compileHint, &globalSignal, &SignalConnection::compileHint);
    connect(this, &CompileProcess::compileError, &globalSignal, &SignalConnection::compileError);
    connect(this, &CompileProcess::compileFinished, &globalSignal, &SignalConnection::compileFinished);
    connect(this, &CompileProcess::VVPStarted, &globalSignal, &SignalConnection::VVPStarted);
    connect(this, &CompileProcess::VVPFinished, &globalSignal, &SignalConnection::VVPFinished);

    // 设置日志文件
    inputStream.setDevice(&inputLog);
    inputStream.setEncoding(QStringConverter::Utf8);
    outputStream.setDevice(&outputLog);
    outputStream.setEncoding(QStringConverter::Utf8);

    m_isLogEnabled = m_config->isLogEnabled();
    m_isLoadHierarchyEnabled = m_config->isLoadHierarchyEnabled();
}

CompileProcess::~CompileProcess()
{
    closeLog();
}

void CompileProcess::runCompile(const QList<Source *> &files, Project *project, bool merge)
{
    if (m_isStarted || files.isEmpty() || !project) {
        return;
    }
    emit compileStarted();
    QTime startTime = QTime::currentTime();
    QList<HierarchyModel *> totalModels;
    if (merge) { // 将文件整合到一个命令中
        QStringList filePathList;
        for (Source *file : files) {
            QString filePath = file->filePath();
            // 将绝对路径变为相对路径，缩短命令长度
            if (QDir(QFileInfo(filePath).absolutePath()) == QDir(project->sourcePath())) {
                filePath = QDir(project->compilePath()).relativeFilePath(filePath);
            }
            filePathList << filePath;
        }
        QString command = QString("iverilog -o %1.out %2").arg(project->projectName()).arg(filePathList.join(' '));
        emit compileInput(command);
        currentError.clear();
        currentOutput.clear();
        startCommand(command); // 执行命令
        bool runSuccessful = waitForStarted();
        waitForReadyRead(m_config->iverilogTimeout());
        waitForFinished(m_config->iverilogTimeout());
        if (thread()->isInterruptionRequested()) {
            return;
        }
        if (!currentError.isEmpty()) { // 出现错误
            project->saveProject();
            emit compileFinished(totalModels);
            return;
        }
        if (currentError.isEmpty() && currentOutput.isEmpty() && runSuccessful) {
            static const QString defaultOutput = QStringLiteral("Compilation succeeeded. 0 Error(s), 0 Warning(s).");
            emit compileOutput(defaultOutput);
        }
        if (m_isLoadHierarchyEnabled) {
            QList<HierarchyModel *> models = readVVPFile(project->projectName() + ".out", project);
            for (HierarchyModel *model : models) {
                if (model->file()) {
                    model->file()->setCompiledFilePath(QDir(project->compilePath()).absoluteFilePath(project->projectName() + ".out"));
                    model->file()->projectItem()->setItemType(ProjectItem::Compiled);
                }
            }
            totalModels << models;
        }
    } else { // 分开编译
        for (Source *file : files) {
            QString filePath = file->filePath();
            if (QDir(QFileInfo(filePath).absolutePath()) == QDir(project->sourcePath())) {
                filePath = QDir(project->compilePath()).relativeFilePath(filePath);
            }
            QString cmd = QString("iverilog %1").arg(filePath);
            emit compileInput(cmd);
            currentError.clear();
            currentOutput.clear();
            startCommand(cmd);
            bool runSuccessful = waitForStarted() || (error() != QProcess::UnknownError);
            waitForReadyRead(m_config->iverilogTimeout());
            waitForFinished(m_config->iverilogTimeout());
            if (thread()->isInterruptionRequested()) {
                return;
            }
            if (!currentError.isEmpty()) {
                file->projectItem()->setItemType(ProjectItem::ErrorCompiled);
                file->setCompiledFilePath(QString());
                file->setWaveFilePath(QString());
                continue;
            }

            if (currentError.isEmpty() && currentOutput.isEmpty() && runSuccessful) {
                static const QString defaultOutput = QStringLiteral("Compilation succeeeded. 0 Error(s), 0 Warning(s).");
                emit compileOutput(defaultOutput);
            }

            QString outputFileName = QDir(project->compilePath()).absoluteFilePath(m_config->outputFileName());
            file->setCompiledFilePath(outputFileName.replace('*', file->fileBaseName()));
            file->projectItem()->setItemType(ProjectItem::Compiled);
            if (m_isLoadHierarchyEnabled) {
                QList<HierarchyModel *> models = readVVPFile(file->compiledFilePath(), project);
                file->projectItem()->setHierModels(models);
                totalModels << models;
            }
        }
    }
    project->saveProject();
    int compileTime = startTime.msecsTo(QTime::currentTime());
    emit compileHint(QString("Compilation time: %1(ms)").arg(compileTime));
    emit compileFinished(totalModels);
}

void CompileProcess::runVVP(Source *file, Project *project)
{
    if (m_isStarted) {
        return;
    }
    emit VVPStarted();
    QTime startTime = QTime::currentTime();
    QString filePath = file->compiledFilePath();
    if (QDir(QFileInfo(filePath).absolutePath()) == QDir(project->compilePath())) {
        filePath = QDir(project->compilePath()).relativeFilePath(filePath);
    }
    QString cmd = QString("vvp -n %1 -lxt2").arg(filePath);
    emit compileInput(cmd);
    currentError.clear();
    currentOutput.clear();
    startCommand(cmd);
    bool runSuccessful = waitForStarted();
    waitForReadyRead(m_config->vvpTimeout());
    waitForFinished(m_config->vvpTimeout());
    if (thread()->isInterruptionRequested()) {
        return;
    }
    if (!currentError.isEmpty()) {
        file->projectItem()->setItemType(ProjectItem::ErrorRun);
        file->setWaveFilePath(QString());
        return;
    }
    QStringList outputFileName;
    static QRegularExpression re_output(R"(dumpfile\s+(.*?)\s+opened)");
    QRegularExpressionMatchIterator i = re_output.globalMatch(currentOutput);
    while (i.hasNext()) {
        QString path = i.next().captured(1);
        if (path.contains(' ') || path.contains('\t')) {
            emit compileError("<Program Error>: dumpfile's name contains space.");
            return;
        }
        outputFileName << QDir(project->compilePath()).absoluteFilePath(path);
    }
    if (currentError.isEmpty() && currentOutput.isEmpty() && runSuccessful) {
        emit compileOutput(QStringLiteral("VVP running succeeeded. 0 Error(s), 0 Warning(s)."));
    }
    file->setWaveFilePath(outputFileName.join(' '));
    file->projectItem()->setItemType(ProjectItem::Run);
    project->saveProject();    
    int compileTime = startTime.msecsTo(QTime::currentTime());
    emit compileHint(QString("VVP running time: %1(ms)").arg(compileTime));
    emit VVPFinished();
}

void CompileProcess::runAnyCommand(const QString &inputCommand)
{
    if (m_isStarted) {
        return;
    }
    emit compileStarted();
    QTime startTime = QTime::currentTime();
    QStringList commandList = inputCommand.split("\n");
    for (const QString &command : commandList) {
        if (command.isEmpty()) {
            continue;
        }
        emit compileInput(command);
        startCommand(command);
        waitForStarted();
        waitForReadyRead(m_config->iverilogTimeout());
        waitForFinished(m_config->iverilogTimeout());
        if (thread()->isInterruptionRequested()) {
            return;
        }
    }
    int compileTime = startTime.msecsTo(QTime::currentTime());
    emit compileHint(QString("Command running time: %1(ms)").arg(compileTime));
    emit compileFinished(QList<HierarchyModel *>());
}

void CompileProcess::startCommand(const QString &command, OpenMode mode)
{
    QStringList commandList = QProcess::splitCommand(command);
    QString programName = QFileInfo(commandList.first()).baseName();
    if (programName == "iverilog") {
        iverilogCmdTransform(commandList);
    } else if (programName == "vvp") {
        vvpCmdTransform(commandList);
    } else {
        QProcess::startCommand(command, mode);
        return;
    }

    QString newCommand = commandList.join(' ');
    QProcess::startCommand(newCommand, mode);
}

void CompileProcess::setWorkingDirectory(const QString &dir)
{
    closeLog();
    inputLog.setFileName(dir + "/compileInput.log");
    outputLog.setFileName(dir + "/compileOutput.log");
    openLog();
    QProcess::setWorkingDirectory(dir);
}

void CompileProcess::openLog()
{
    if (!inputLog.isOpen()) {
        inputLog.open(QFile::WriteOnly | QFile::Truncate);
    }
    if (!outputLog.isOpen()) {
        outputLog.open(QFile::WriteOnly | QFile::Truncate);
    }
}

void CompileProcess::closeLog()
{
    if (inputLog.isOpen()) {
        inputLog.close();
    }
    if (outputLog.isOpen()) {
        outputLog.close();
    }
}

void CompileProcess::iverilogCmdTransform(QStringList &commands)
{
    static QRegularExpression re_cmdFilePath(QStringLiteral(R"(^-c)"));
    if (commands.filter(re_cmdFilePath).isEmpty()) {
        if (!m_config->commandFilePath().isEmpty()) {
            commands.insert(1, "-c " + m_config->commandFilePath());
        }
    }

    static QRegularExpression re_debugFlag(QStringLiteral(R"(^-d)"));
    if (commands.filter(re_debugFlag).isEmpty()) {
        for (const ProgramConfig::DebugFlag &flag : m_config->debugFlags()) {
            commands.insert(1, "-d " + m_config->debugFlagStr[flag]);
        }
    }

    static QRegularExpression re_generationFlag(QStringLiteral(R"(^-g)"));
    if (commands.filter(re_generationFlag).isEmpty()) {
        for (const ProgramConfig::GenerationFlag &flag : m_config->generationFlags()) {
            commands.insert(1, "-g " + m_config->generationFlagStr[flag]);
        }
    }

    static QRegularExpression re_ignoreMissingModule(QStringLiteral(R"(^-i)"));
    if (commands.filter(re_ignoreMissingModule).isEmpty()) {
        if (m_config->ifIgnoreMissingModule()) {
            commands.insert(1, "-i");
        }
    }

    static QRegularExpression re_VPIModules(QStringLiteral(R"(^-L)"));
    if (commands.filter(re_VPIModules).isEmpty()) {
        for (const QString &path : m_config->VPIModuleDirs()) {
            if (!path.isEmpty()) {
                commands.insert(1, "-L " + path);
            }
        }
    }

    static QRegularExpression re_libraryFiles(QStringLiteral(R"(^-I)"));
    if (commands.filter(re_libraryFiles).isEmpty()) {
        commands.insert(1, "-I ../src");
        for (const QString &path : m_config->libraryFilePaths()) {
            if (!path.isEmpty()) {
                commands.insert(1, "-I " + path);
            }
        }
    }

    static QRegularExpression re_outputFile(QStringLiteral(R"(^-o)"));
    if (commands.filter(re_outputFile).isEmpty()) {
        QString outputFileName = m_config->outputFileName();
        outputFileName.replace('*', QFileInfo(commands.last()).baseName());
        commands.insert(1, "-o " + outputFileName);
    }

    static QRegularExpression re_libraryPaths(QStringLiteral(R"(^-y)"));
    if (commands.filter(re_libraryPaths).isEmpty()) {
        commands.insert(1, "-y ../src");
        for (const QString &path : m_config->libraryDirs()) {
            if (!path.isEmpty()) {
                commands.insert(1, "-y " + path);
            }
        }
    }

    static QRegularExpression re_macros(QStringLiteral(R"(^-D)"));
    if (commands.filter(re_macros).isEmpty()) {
        for (const QString &macro : m_config->macros()) {
            if (!macro.isEmpty()) {
                commands.insert(1, "-D " + macro);
            }
        }
    }
}

void CompileProcess::vvpCmdTransform(QStringList &commands)
{
    commands.append(m_config->vvpExtra());
}

QList<HierarchyModel *> CompileProcess::readVVPFile(const QString &fileName, Project *project)
{
    if (!project || fileName.isEmpty()) {
        return QList<HierarchyModel *>();
    }
    QFile VVPFile(QDir(project->compilePath()).absoluteFilePath(fileName));
    if (!VVPFile.open(QIODevice::ReadOnly)) {
        return QList<HierarchyModel *>();
    }
    static QRegularExpression re_filename_title(R"(:file_names\s(\d+))");
    static QRegularExpression re_filename(R"(\"(.*?)\";)");
    static QRegularExpression re_topscope(R"(([a-zA-Z\$_\<\>][a-zA-Z0-9\.\$_\<\>]*)\s\.scope\smodule\s?,\s?\"(.*?)\"\s\"(.*?)\"\s([0-9]+)\s[0-9]+\s?;)");
    static QRegularExpression re_scope(R"(([a-zA-Z\$_\<\>][a-zA-Z0-9\.\$_\<\>]*)\s\.scope\smodule\s?,\s?\"(.*?)\"\s\"(.*?)\"\s([0-9]+)\s[0-9]+\s?,\s?([0-9]+)\s[0-9]+\s[0-1]\s?,\s?([a-zA-Z\$_\<\>][a-zA-Z0-9\.\$_\<\>]*)\s?;)");
    static QRegularExpression re_var(R"([a-zA-Z\$_\<\>][a-zA-Z0-9\.\$_\<\>]*\s\.(?:var|net)[/s|/2u|/2s|/real]?\s\"(.*?)\"\s?,\s?([0-9]+)\s([0-9]+)\s?.*?;)");

    QList<VVPFormat *> moduleList;
    VVPFormat *curModule = nullptr;
    bool hitFileName = false;
    QStringList fileNameList;
    while(!VVPFile.atEnd()) {
        QString curLine = VVPFile.readLine();
        QRegularExpressionMatch match = re_topscope.match(curLine);
        if (match.hasMatch()) {
            curModule = new VVPFormat{match.captured(1),
                                      match.captured(2),
                                      match.captured(3),
                                      match.captured(4).toInt(),
                                      match.captured(4).toInt(),
                                      QString(),
                                      new QStandardItemModel};
            moduleList.append(curModule);
            continue;
        }
        match = re_scope.match(curLine);
        if (match.hasMatch()) {
            curModule = new VVPFormat{match.captured(1),
                                      match.captured(2),
                                      match.captured(3),
                                      match.captured(4).toInt(),
                                      match.captured(5).toInt(),
                                      match.captured(6),
                                      new QStandardItemModel};
            moduleList.append(curModule);
            continue;
        }
        match = re_var.match(curLine);
        if (match.hasMatch()) {
            if (!curModule) {
                continue;
            }
            QStandardItem *item = new QStandardItem;
            item->setData(match.captured(1), Qt::UserRole);
            if (match.captured(2).toInt() > 0) {
                item->setData(match.captured(1) + QString("[%1:%2]").arg(match.captured(2),match.captured(3)), Qt::DisplayRole);
            } else {
                item->setData(match.captured(1), Qt::DisplayRole);
            }
            curModule->variableListModel->appendRow(item);
            continue;
        }
        match = re_filename_title.match(curLine);
        if (match.hasMatch()) {
            hitFileName = true;
            continue;
        }
        if (hitFileName) {
            match = re_filename.match(curLine);
            if (match.hasMatch()) {
                fileNameList.append(match.captured(1));
            }
        }
    }
    int fileLen = fileNameList.length();
    QList<Source *> corFiles(fileLen, nullptr);
    QDir parentDir(project->compilePath());
    for (int i = 0; i < fileLen; ++i) {
        QString str = fileNameList[i];
        for (Source *file : project->fileList()) {
            if (QDir(parentDir.absoluteFilePath(str)) == QDir(file->filePath())) {
                corFiles[i] = file;
                break;
            }
        }
    }
    int moduleLen = moduleList.length();
    QList<HierarchyItem *> itemList(moduleLen);
    for (int i = 0; i < moduleLen; ++i) {
        itemList[i] = new HierarchyItem(moduleList[i]->instanceName, moduleList[i]->definitionName, corFiles[moduleList[i]->definitionIndex]);
        itemList[i]->setVariableModel(moduleList[i]->variableListModel);
    }
    QList<HierarchyModel *> modelList;
    for (int i = 0; i < moduleLen; ++i) {
        if (moduleList[i]->parentLabel.isEmpty()) {
            HierarchyModel *curModel = new HierarchyModel(moduleList[i]->instanceName, corFiles[moduleList[i]->definitionIndex]);
            modelList.append(curModel);
            curModel->appendRow(itemList[i]);
            continue;
        }
        for (int j = 0; j < moduleLen; ++j) {
            if (moduleList[i]->parentLabel == moduleList[j]->label) {
                itemList[j]->appendRow(itemList[i]);
                break;
            }
        }
    }
    VVPFile.close();
    return modelList;
}
